{-# LANGUAGE ApplicativeDo         #-}
{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DerivingStrategies    #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}

{-| @dhall-to-nixpkgs@ is essentially the Dhall analog of @cabal2nix@.

    The following Nix code is required reading for understanding how
    @dhall-to-nixpkgs@ is supposed to work:

    <https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/interpreters/dhall/build-dhall-package.nix>

    The Nixpkgs support for Dhall builds packages without using Dhall's support
    for HTTP(S) imports.  Instead, all remote packages are fetched by Nix
    (e.g. using @pkgs.fetchFromGitHub@) instead of being fetched by Dhall.  In
    fact, the Nix builder enforces that Dhall builds are offline by compiling
    the @dhall@ executable with the @-f-with-http@ configure flag to disable
    support for remote imports and then using that executable to build Dhall
    packages.

    Nix is able to build Dhall packages with remote imports by requiring that
    remote imports are protected with semantic integrity checks and then
    replacing the remote imports with cache hits.  So long as the correct
    dependency is added to `buildDhallPackage`'s `dependencies` argument then
    the builder will ensure that the dependency is present in the cache so that
    the remote import is elided.

    However, using `buildDhallPackage` to author a Dhall package is tedious,
    due to manual steps such as:

    * Discovering all of a Dhall package's remote dependencies and translating
      them to Nix code

    * For GitHub packages, computing the correct revision and hash

    @dhall-to-nixpkgs@ automates the generation of suitable Nix code, taking
    care of details such as these and more.  Note that @dhall-to-nixpkgs@
    technically uses the higher-level @buildDhall{GitHub,Directory}Package@
    utilities instead of the lower-level @buildDhallPackage@ in order to
    simplify the generated code, but the general idea is still the same.

    The Nixpkgs support for Dhall makes a simplifying assumption of a
    "consistent package set", meaning that even though there may be multiple
    buildable versions for a Dhall package there will always be a preferred
    version for every package (analogous to how a Stackage resolver selects
    default package versions for Haskell).  This means that the dependency list
    generated by @dhall-to-nixpkgs@ will not refer to specific versions or
    revisions of dependencies and will instead refer to the default preferred
    version.

    For example, if a package were to depend on both
    @https://prelude.dhall-lang.org/v10.0.0/package.dhall@ and
    @https://prelude.dhall-lang.org/v11.0.0/package.dhall@, @dhall-to-nixpkgs@
    would convert both of them to the same version-less @Prelude@ dependency
    (which would select the default Prelude version).  That in turn would
    inevitably fail to satisfy one or the other dependency.

    This implies that @dhall-to-nixpkgs@ does not support packages that depend
    on multiple versions of the same dependency.  We could technically support
    this, but for now the tool will discourage that.  Users that really want to
    depend on a specific version of a dependency can still manually edit
    the code generated by @dhall-to-nixpkgs@ if they insist.
-}

module Main where

import Control.Applicative              (empty, optional, (<|>))
import Control.Monad.IO.Class           (MonadIO (..))
import Control.Monad.Morph              (hoist)
import Control.Monad.Trans.Class        (lift)
import Control.Monad.Trans.State.Strict (StateT)
import Data.Aeson                       (FromJSON)
import Data.List.NonEmpty               (NonEmpty (..))
import Data.Maybe                       (mapMaybe)
import Data.Text                        (Text)
import Data.Void                        (Void)
import Dhall.Crypto                     (SHA256Digest (..))
import Dhall.Import                     (Status (..), stack)
import Dhall.Parser                     (Src)
import GHC.Generics                     (Generic)
import Lens.Micro                       (rewriteOf)
import Lens.Micro.Mtl                   (zoom)
import Network.URI                      (URI (..), URIAuth (..))
import Nix.Expr.Shorthands              ((@.), (@@))
import Nix.Expr.Types                   (NExpr)
import Options.Applicative              (Parser, ParserInfo)
import Prelude                          hiding (FilePath)
import System.Exit                      (ExitCode (..))
import Text.Megaparsec                  (Parsec)
import Turtle                           (FilePath, Shell, fp, (</>))

import Dhall.Core
    ( Expr (..)
    , File (..)
    , Import (..)
    , ImportHashed (..)
    , ImportMode (..)
    , ImportType (..)
    , URL (..)
    )

import qualified Control.Foldl                    as Foldl
import qualified Control.Monad.Trans.State.Strict as State
import qualified Data.Aeson                       as Aeson
import qualified Data.ByteString.Base16           as Base16
import qualified Data.ByteString.Base64           as Base64
import qualified Data.ByteString.Char8            as ByteString.Char8
import qualified Data.Foldable                    as Foldable
import qualified Data.List.NonEmpty               as NonEmpty
import qualified Data.Text                        as Text
import qualified Data.Text.Encoding               as Text.Encoding
import qualified Data.Text.IO                     as Text.IO
import qualified Dhall.Core
import qualified Dhall.Import
import qualified Dhall.Parser
import qualified GHC.IO.Encoding
import qualified NeatInterpolation
import qualified Network.URI                      as URI
import qualified Nix.Expr.Shorthands              as Nix
import qualified Nix.Pretty
import qualified Options.Applicative              as Options
import qualified Prettyprinter.Render.Text        as Prettyprint.Text
import qualified System.Exit
import qualified System.IO
import qualified Text.Megaparsec                  as Megaparsec
import qualified Text.Megaparsec.Char             as Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer       as Megaparsec.Char.Lexer
import qualified Turtle

data Options
    = OptionsGitHub GitHub
    | OptionsDirectory Directory

data GitHub = GitHub
    { name :: Maybe Text
    , uri :: Text
    , rev :: Maybe Text
    , hash :: Maybe Text
    , fetchSubmodules :: Bool
    , directory :: FilePath
    , file :: FilePath
    , source :: Bool
    , document :: Bool
    }

data Directory = Directory
    { name :: Maybe Text
    , directory :: FilePath
    , file :: FilePath
    , source :: Bool
    , document :: Bool
    , fixedOutputDerivations :: Bool
    }

data NixPrefetchGit = NixPrefetchGit
    { url :: Text
    , rev :: Text
    , path :: Text
    , sha256 :: Text
    }
    deriving stock (Generic)
    deriving anyclass (FromJSON)

parseOptions :: Parser Options
parseOptions =
        subcommand
            "github"
            "Create a Nix package from a GitHub repository"
            (fmap OptionsGitHub parseGitHub)
    <|> subcommand
            "directory"
            "Create a Nix package from a local directory"
            (fmap OptionsDirectory parseDirectory)
  where
    subcommand name description parser =
        Options.hsubparser
            (   Options.command name parserInfo
            <>  Options.metavar name
            )
      where
        parserInfo =
            Options.info parser
                (   Options.fullDesc
                <>  Options.progDesc description
                )

parseFile :: Parser FilePath
parseFile =
    Options.strOption
        (   Options.long "file"
        <>  Options.help "File to import, relative to the top-level directory"
        <>  Options.value "package.dhall"
        <>  Options.action "file"
        )

parseSource :: Parser Bool
parseSource =
    Options.switch
        (   Options.long "source"
        <>  Options.help "Configure the Nix package to include source code"
        )

parseDocument :: Parser Bool
parseDocument =
    Options.switch
        (   Options.long "document"
        <>  Options.help "Generate documentation for the Nix package"
        )

parseFixedOutputDerivations :: Parser Bool
parseFixedOutputDerivations =
    Options.switch
        (   Options.long "fixed-output-derivations"
        <>  Options.help "Translate Dhall remote imports to Nix fixed-output derivations"
        )

parseName :: Parser (Maybe Text)
parseName =
    optional
        (Options.strOption
            (   Options.long "name"
            <>  Options.help "Name for the Nix derivation"
            )
        )

parseGitHub :: Parser GitHub
parseGitHub = do
    name <- parseName

    uri <- Options.strArgument (Options.metavar "URL")

    rev <- do
        optional
            (Options.strOption
                (   Options.long "rev"
                <>  Options.help "Git revision to use"
                )
            )

    hash <- do
        optional
            (Options.strOption
                (   Options.long "hash"
                <>  Options.help "Expected SHA256 hash"
                )
            )

    fetchSubmodules <-
        Options.switch
            (   Options.long "fetch-submodules"
            <>  Options.help "Fetch git submodules"
            )

    directory <-
        Options.strOption
            (   Options.long "directory"
            <>  Options.help "Subdirectory containing the Dhall package"
            <>  Options.value ""
            <>  Options.action "directory"
            )

    file <- parseFile

    source <- parseSource

    document <- parseDocument

    return GitHub{..}

parseDirectory :: Parser Directory
parseDirectory = do
    name <- parseName

    directory <- Options.strArgument (Options.metavar "DIRECTORY")

    file <- parseFile

    source <- parseSource

    document <- parseDocument

    fixedOutputDerivations <- parseFixedOutputDerivations

    return Directory{..}

parserInfoOptions :: ParserInfo Options
parserInfoOptions =
    Options.info
        (Options.helper <*> parseOptions)
        (   Options.progDesc "Convert a Dhall project to a Nix package"
        <>  Options.fullDesc
        )

main :: IO ()
main = do
    GHC.IO.Encoding.setLocaleEncoding System.IO.utf8

    options <- Options.execParser parserInfoOptions

    case options of
        OptionsGitHub    github    -> githubToNixpkgs    github
        OptionsDirectory directory -> directoryToNixpkgs directory

-- | Convenient utility for generating command-line options
toListWith :: (a -> [ Text ]) -> Maybe a -> [ Text ]
toListWith f (Just x ) = f x
toListWith _  Nothing  = [ ]

nub :: Ord a => [a] -> [a]
nub = Foldl.fold Foldl.nub

{-| The Nixpkgs support for Dhall essentially replaces all remote imports with
    cache hits, but doing so implies that all remote imports must be protected
    by an integrity check.

    This function finds all remote imports that are transitive dependencies of
    the given expression, failing if any of them are missing integrity checks.
-}
findExternalDependencies :: Expr Src Import -> StateT Status Shell (URL, SHA256Digest)
findExternalDependencies expression = do
    -- This is a best-effort attempt to pick an import alternative if there is
    -- more than one
    let pickAlt :: Expr Src Import -> Maybe (Expr Src Import)
        pickAlt (ImportAlt e0 e1)
            -- If only the latter import has an integrity check, then select
            -- that
            | Embed Import{ importHashed = ImportHashed{ hash = Nothing } } <- Dhall.Core.shallowDenote e0
            , Embed Import{ importHashed = ImportHashed{ hash = Just _  } } <- Dhall.Core.shallowDenote e1 =
                Just e1
            -- Otherwise prefer the first import
            | otherwise =
                Just e0
        pickAlt _ =
            Nothing

    let rewrittenExpression :: Expr Src Import
        rewrittenExpression =
            rewriteOf Dhall.Core.subExpressions pickAlt expression

    import_ <- lift (Turtle.select (Foldable.toList rewrittenExpression))

    parent :| _ <- zoom stack State.get

    child <- hoist liftIO (Dhall.Import.chainImport parent import_)

    let Import{ importHashed, importMode } = Dhall.Import.chainedImport child

    let ImportHashed{ hash, importType } = importHashed

    case importMode of
        Code     -> return ()
        RawText  -> return ()
        RawBytes -> return ()
        Location -> empty  -- "as Location" imports aren't real dependencies

    case importType of
        Missing ->
            empty

        Env {} ->
            empty

        Remote url ->
            case hash of
                Just sha256 ->
                    return (url, sha256)
                Nothing ->
                    die (MissingSemanticIntegrityCheck url)

        Local filePrefix file -> do
            filepath <- liftIO (Dhall.Import.localToPath filePrefix file)

            expressionText <- liftIO (Text.IO.readFile filepath)

            parsedExpression <- Dhall.Core.throws (Dhall.Parser.exprFromText filepath expressionText)

            zoom stack (State.modify (NonEmpty.cons child))

            findExternalDependencies parsedExpression

data Dependency = Dependency
    { functionParameter :: Maybe (Text, Maybe NExpr)
      -- ^ Function parameter used to bring the dependency into scope for the
      --   Nix package.
      --
      --   This is 'Nothing' when 'fixedOutputDerivations' is enabled, since these
      --   dependencies don't need to passed in as arguments. This is 'Just'
      --   when 'fixedOutputDerivations' is not enabled.
      --
      --   The @'Maybe' 'NExpr'@ is always 'Nothing', but we
      --   include it here for convenience
    , dependencyExpression :: NExpr
      -- ^ The dependency expression to include in the dependency list.
      --
      -- 'dependencyToNix' will create an expression of the following form.
      -- This is called when 'fixedOutputDerivations' is 'False':
      --
      --   > someDependency.override { file = "./someFile.dhall" }
      --
      -- 'dependencyToNixAsFOD' will create an expression of the following form.
      -- This is called when 'fixedOutputDerivations' is 'True':
      --
      --   > buildDhallUrl {
      --   >   url = "https://some.url.to/a/dhall/file.dhall";
      --   >   hash = "sha256-ZTSiQUXpPbPfPvS8OeK6dDQE6j6NbP27ho1cg9YfENI=";
      --   >   dhallHash =
      --   >     "sha256:6534a24145e93db3df3ef4bc39e2ba743404ea3e8d6cfdbb868d5c83d61f10d2";
      --   > }
    }
    deriving stock Show

-- | Convert a 'URL' and integrity check to a Nix 'Dependency' that uses the
-- Nix function @buildDhallUrl@ to build.
--
-- This function will create a Nix dependency of the form:
--
--   > buildDhallUrl {
--   >   url = "https://some.url.to/a/dhall/file.dhall";
--   >   hash = "sha256-ZTSiQUXpPbPfPvS8OeK6dDQE6j6NbP27ho1cg9YfENI=";
--   >   dhallHash =
--   >     "sha256:6534a24145e93db3df3ef4bc39e2ba743404ea3e8d6cfdbb868d5c83d61f10d2";
--   > }
--
-- The @hash@ argument is an SRI hash that Nix understands.  The @dhallHash@
-- argument is a base-16-encoded hash that Dhall understands.
dependencyToNixAsFOD :: URL -> SHA256Digest -> IO Dependency
dependencyToNixAsFOD url (SHA256Digest shaBytes) = do
    let functionParameter = Nothing

    let dhallHash =
            "sha256:" <> ByteString.Char8.unpack (Base16.encode shaBytes)

    let nixSRIHash =
            "sha256-" <> ByteString.Char8.unpack (Base64.encode shaBytes)

    let dependencyExpression =
                "buildDhallUrl"
            @@  Nix.attrsE
                    [ ("url", Nix.mkStr $ Dhall.Core.pretty url)
                    , ("hash", Nix.mkStr $ Text.pack nixSRIHash)
                    , ("dhallHash", Nix.mkStr $ Text.pack dhallHash)
                    ]

    return Dependency{..}

{-| The Nixpkgs support for Dhall implements two conventions that
    @dhall-to-nixpkgs@ depends on:

    * Packages are named after their repository name
    * You can import a specific file using `packageName.override { file = …; }`

    This function is responsible for converting Dhall imports to package
    names and files that follow this convention.  For example, given a Dhall
    import like:

        https://raw.githubusercontent.com/EarnestResearch/dhall-packages/master/kubernetes/k8s/1.14.dhall

    ... this will create the corresponding Nix dependency of the form:

        dhall-packages.override { file = "kubernetes/k8s/1.14.dhall"; }
-}
dependencyToNix :: URL -> IO Dependency
dependencyToNix url@URL{ authority, path } = do
    let prelude = "Prelude"

    case authority of
        "raw.githubusercontent.com" -> do
            let File{ directory, file } = path

            let Dhall.Core.Directory{ components } = directory

            case reverse (file : components) of
                -- Special case to recognize a Prelude import and treat it as if
                -- it were an import of prelude.dhall-lang.org
                "dhall-lang" : "dhall-lang" : _rev : "Prelude" : rest -> do
                    let fileArgument = Text.intercalate "/" rest

                    let functionParameter = Just (prelude, Nothing)

                    let dependencyExpression =
                                (Nix.mkSym prelude @. "overridePackage")
                            @@  Nix.attrsE
                                    [ ("file", Nix.mkStr fileArgument ) ]

                    return Dependency{..}

                _owner : repo : _rev : rest -> do
                    let fileArgument = Text.intercalate "/" rest

                    let functionParameter = Just (repo, Nothing)

                    let dependencyExpression =
                                (Nix.mkSym repo @. "overridePackage")
                            @@  Nix.attrsE
                                    [ ("file", Nix.mkStr fileArgument ) ]

                    return Dependency{..}

                _ -> do
                    die (NotAValidGistRepositoryURL url)

        "gist.githubusercontent.com" -> do
            let File{ directory, file } = path

            let Dhall.Core.Directory{ components } = directory

            case reverse (file : components) of
                owner : hash : "raw" : _rev : rest -> do
                    let fileArgument = Text.intercalate "/" rest

                    let package = owner <> "_" <> hash

                    let functionParameter = Just (package, Nothing)

                    let dependencyExpression =
                                (Nix.mkSym package @. "overridePackage")
                            @@  Nix.attrsE
                                    [ ("file", Nix.mkStr fileArgument ) ]

                    return Dependency{..}

                _ -> do
                    die (NotAValidGitHubRepositoryURL url)

        "prelude.dhall-lang.org" -> do
            let File{ directory, file } = path

            let Dhall.Core.Directory{ components } = directory

            let component :: Parsec Void Text Integer
                component = Megaparsec.Char.Lexer.decimal

            let version :: Parsec Void Text ()
                version = do
                    _ <- Megaparsec.Char.char 'v'
                    _ <- component
                    _ <- Megaparsec.Char.char '.'
                    _ <- component
                    _ <- Megaparsec.Char.char '.'
                    _ <- component
                    return ()

            let pathComponents =
                    case reverse (file : components) of
                        first : rest
                            -- Ignore the version.  The Nixpkgs support assumes
                            -- a curated set of package versions.
                            | Just _ <- Megaparsec.parseMaybe version first ->
                                rest
                        rest ->
                            rest

            let fileArgument = Text.intercalate "/" pathComponents

            let functionParameter = Just (prelude, Nothing)

            let dependencyExpression =
                        (Nix.mkSym prelude @. "overridePackage")
                    @@  Nix.attrsE

                            [ ("file", Nix.mkStr fileArgument) ]

            return Dependency{..}
        _ -> do
            die (UnsupportedDomainDependency url authority)

-- | Turn a list of 'Dependency's into an argument list for the generated Nix
-- function.
--
-- The following 'makeNixFunctionParams' call:
--
-- @@
--   'makeNixFunctionParams'
--     \"buildDhallDirectoryPackage\"
--     [ 'Dependency' ('Just' (\"Prelude\", 'Nothing')) ...
--     , 'Dependency' ('Just' (\"Prelude\", 'Nothing')) ...
--     , 'Dependency' 'Nothing' ...
--     , 'Dependency' ('Just' (\"example-repo\", 'Nothing')) ...
--     ]
-- @@
--
-- will generate an argument list like the following:
--
-- > { buildDhallDirectoryPackage, buildDhallUrl, Prelude, example-repo }:
--
-- Note that identical 'functionParameter's will be collapsed into a single
-- parameter (like @Prelude@ above).
--
-- @buildDhallUrl@ will be added as an argument only if there is a 'Dependency'
-- with a 'Nothing' value for 'functionalParameter'.
makeNixFunctionParams :: Text -> [Dependency] -> [(Text, Maybe NExpr)]
makeNixFunctionParams buildDhallFuncName nixDependencies =
    let containsBuildDhallUrlDependency =
            any (\dep -> functionParameter dep == Nothing) nixDependencies

        buildDhallUrlParam =
            if containsBuildDhallUrlDependency
                then [ ("buildDhallUrl", Nothing) ]
                else [ ]

    in  (   [ (buildDhallFuncName, Nothing) ]
        <>  buildDhallUrlParam
        <>  nub (mapMaybe functionParameter nixDependencies)
        )

githubToNixpkgs :: GitHub -> IO ()
githubToNixpkgs GitHub{ name, uri, rev = maybeRev, hash, fetchSubmodules, directory, file, source, document } = do
    URI{ uriScheme, uriAuthority = Just URIAuth{ uriUserInfo, uriRegName, uriPort }, uriPath, uriQuery, uriFragment } <- do
        case URI.parseAbsoluteURI (Text.unpack uri) of
            Nothing -> die (RepositoryIsNotAValidURI uri)
            Just u  -> return u

    case uriScheme of
        "https:" -> return ()
        _        -> die (UnsupportedURIScheme uri uriScheme)

    case uriRegName of
        "github.com"      -> return ()
        "gist.github.com" -> return ()
        _                 -> die (UnsupportedDomain uri uriRegName)

    case uriPort of
        "" -> return ()
        _  -> die (UnsupportedPort uri uriPort)

    case uriQuery of
        "" -> return ()
        _  -> die (UnsupportedQuery uri uriQuery)

    case uriFragment of
        "" -> return ()
        _  -> die (UnsupportedFragment uri uriFragment)

    let githubBase = Text.pack (uriUserInfo <> uriRegName <> uriPort)

    let parsePath :: Parsec Void String (Text, Text)
        parsePath = do
            _ <- Megaparsec.Char.char '/'

            owner <- Megaparsec.takeWhile1P Nothing (/= '/')

            _ <- Megaparsec.Char.char '/'

            repo <- Megaparsec.takeWhile1P Nothing (/= '.')

            optional (Megaparsec.Char.string ".git")

            return (Text.pack owner, Text.pack repo)

    (owner, repo) <- case Megaparsec.parseMaybe parsePath uriPath of
        Nothing -> die (NotAValidGitHubRepository uri)

        Just (owner, repo) -> do
            return (owner, repo)

    let baseUrl =
            Text.pack uriScheme <> "//" <> githubBase <> "/" <> owner <> "/" <> repo

    (rev, sha256, repository) <- case maybeRev of
        Just r | not fetchSubmodules -> do
            let archiveURL = baseUrl <> "/archive/" <> r <> ".tar.gz"

            let args =  [ "--unpack"
                        , "--type", "sha256"
                        , "--print-path"
                        , "--name", repo
                        , archiveURL
                        ]
                    <>  toListWith (\t -> [ t ]) hash

            (exitCode, text) <- Turtle.procStrict "nix-prefetch-url" args empty

            case exitCode of
                ExitSuccess   -> return ()
                ExitFailure _ -> die (FailedToFetchGitHubArchive args)

            case Text.lines text of
                [ sha256, path ] -> return (r, sha256, Text.unpack path)
                _                -> die (FailedToParseNixPrefetchURL args)

        _ -> do
            let args =  [ "--url", baseUrl <> ".git"
                        , "--quiet"
                        ]
                    <>  toListWith (\t -> [ "--rev", t ]) maybeRev
                    <>  toListWith (\t -> [ "--hash", t ]) hash
                    <>  (if fetchSubmodules then [ "--fetch-submodules" ] else [])

            (exitCode, text) <- Turtle.procStrict "nix-prefetch-git" args empty

            case exitCode of
                ExitSuccess   -> return ()
                ExitFailure _ -> die (FailedToCloneRepository args)

            let bytes = Text.Encoding.encodeUtf8 text

            NixPrefetchGit{ rev, sha256, path } <- case Aeson.eitherDecodeStrict' bytes of
                Left message -> do
                    die (FailedToParseNixPrefetchGit args message)
                Right n -> do
                    return n

            return (rev, sha256, Text.unpack path)

    let finalName =
            case name of
                Nothing -> repo
                Just n  -> n

    let expressionFile = repository </> directory </> file

    let baseDirectory = Turtle.directory expressionFile

    exists <- Turtle.testfile expressionFile

    if exists
        then return ()
        else die (MissingFile expressionFile)

    expressionText <- Text.IO.readFile expressionFile

    expression <- Dhall.Core.throws (Dhall.Parser.exprFromText expressionFile expressionText)

    let status = Dhall.Import.emptyStatus baseDirectory

    dependencies <- Turtle.reduce Foldl.nub (State.evalStateT (findExternalDependencies expression) status)

    nixDependencies <- traverse (\(url, _sha256) -> dependencyToNix url) dependencies

    let buildDhallGitHubPackage = "buildDhallGitHubPackage"

    let functionParams =
            makeNixFunctionParams buildDhallGitHubPackage nixDependencies

    let nixExpression =
            Nix.mkFunction
                (Nix.mkParamset functionParams False)
                (   Nix.mkSym buildDhallGitHubPackage
                @@  Nix.attrsE
                        [ ("name", Nix.mkStr finalName)
                        , ("githubBase", Nix.mkStr githubBase)
                        , ("owner", Nix.mkStr owner)
                        , ("repo", Nix.mkStr repo)
                        , ("rev", Nix.mkStr rev)
                        , ("fetchSubmodules", Nix.mkBool fetchSubmodules)
                        -- TODO: Support `private` / `varBase` options
                        , ("sha256", Nix.mkStr sha256)
                        , ("directory", Nix.mkStr (Turtle.format fp directory))
                        , ("file", Nix.mkStr (Turtle.format fp file))
                        , ("source", Nix.mkBool source)
                        , ("document", Nix.mkBool document)
                        , ("dependencies", Nix.mkList (nub (fmap dependencyExpression nixDependencies)))
                        ]
                )

    Prettyprint.Text.putDoc ((Nix.Pretty.prettyNix nixExpression) <> "\n")

directoryToNixpkgs :: Directory -> IO ()
directoryToNixpkgs Directory{ name, directory, file, source, document, fixedOutputDerivations } = do
    let finalName =
            case name of
                Nothing -> Turtle.format fp (Turtle.dirname directory)
                Just n  -> n

    let expressionFile = directory </> file

    exists <- Turtle.testfile expressionFile

    if exists
        then return ()
        else do
            die (MissingFile expressionFile)

    expressionText <- Text.IO.readFile expressionFile

    expression <- Dhall.Core.throws (Dhall.Parser.exprFromText expressionFile expressionText)

    let status = Dhall.Import.emptyStatus (Turtle.directory expressionFile)

    dependencies <- Turtle.reduce Foldl.nub (State.evalStateT (findExternalDependencies expression) status)

    let depToNix :: (URL, SHA256Digest) -> IO Dependency
        depToNix (url, sha256) =
            if fixedOutputDerivations
              then dependencyToNixAsFOD url sha256
              else dependencyToNix url

    nixDependencies <- traverse depToNix dependencies

    let buildDhallDirectoryPackage = "buildDhallDirectoryPackage"

    let src | null directory        = directory
            | last directory == '/' = init directory
            | otherwise             = directory

    let functionParams =
            makeNixFunctionParams buildDhallDirectoryPackage nixDependencies

    let nixExpression =
            Nix.mkFunction
                (Nix.mkParamset functionParams False)
                (   Nix.mkSym buildDhallDirectoryPackage
                @@  Nix.attrsE
                        [ ("name", Nix.mkStr finalName)
                        , ("src", Nix.mkPath False src)
                        , ("file", Nix.mkStr (Turtle.format fp file))
                        , ("source", Nix.mkBool source)
                        , ("document", Nix.mkBool document)
                        , ("dependencies", Nix.mkList (nub (fmap dependencyExpression nixDependencies)))
                        ]
                )

    Prettyprint.Text.putDoc ((Nix.Pretty.prettyNix nixExpression) <> "\n")

die :: MonadIO io => Error -> io a
die e = liftIO $ do
    Text.IO.hPutStrLn System.IO.stderr (renderError e)

    System.Exit.exitFailure

data Error
    = MissingSemanticIntegrityCheck URL
    | NotAValidGitHubRepositoryURL URL
    | NotAValidGistRepositoryURL URL
    | UnsupportedDomainDependency URL Text
    | RepositoryIsNotAValidURI Text
    | UnsupportedURIScheme Text String
    | UnsupportedDomain Text String
    | UnsupportedPort Text String
    | UnsupportedQuery Text String
    | UnsupportedFragment Text String
    | NotAValidGitHubRepository Text
    | FailedToFetchGitHubArchive [Text]
    | FailedToParseNixPrefetchURL [Text]
    | FailedToCloneRepository [Text]
    | FailedToParseNixPrefetchGit [Text] String
    | MissingFile FilePath

renderError :: Error -> Text
renderError e = case e of
    MissingSemanticIntegrityCheck url ->
        let dependency = Dhall.Core.pretty url

        in  [NeatInterpolation.text|
Error: Dependency missing a semantic integrity check

Dhall's Nixpkgs requires that all of your remote dependencies are protected by
by semantic integrity checks.  This ensures that Nix can replace the remote
imports with cached imports built by Nix instead of the imports being fetched
via HTTP requests using Dhall.

The following dependency is missing a semantic integrity check:

↳ $dependency
|]

    NotAValidGitHubRepositoryURL url ->
        let dependency = Dhall.Core.pretty url

        in  [NeatInterpolation.text|
Error: Not a valid GitHub repository URL

Your Dhall package appears to depend on the following import:

↳ $dependency

... which is missing one or more path components that a raw GitHub import would
normally have.  The URL should minimally have the following path components:

↳ https://raw.githubusercontent.com/$${owner}/$${repository}/$${revision}/…
|]

    NotAValidGistRepositoryURL url ->
        let dependency = Dhall.Core.pretty url

        in  [NeatInterpolation.text|
Error: Not a valid gist repository URL

Your Dhall package appears to depend on the following import:

↳ $dependency

... which is missing one or more path components that a raw GitHub import would
normally have.  The URL should minimally have the following path components:

↳ https://gist.githubusercontent.com/$${owner}/$${id}/raw/$${revision}/…
|]

    UnsupportedDomainDependency url authority ->
        let dependency = Dhall.Core.pretty url

        in  [NeatInterpolation.text|
Error: Unsupported domain

This tool currently only translates the following domains into Nix dependencies:

* raw.githubusercontent.com
* gist.githubusercontent.com
* prelude.dhall-lang.org

One of the Dhall project's dependencies:

↳ $dependency

... used the following unexpected domain:

↳ $authority

If you would like to support a new domain for Dhall dependencies, please open an
issue here:

↳ https://github.com/dhall-lang/dhall-haskell/issues
|]

    RepositoryIsNotAValidURI uri ->
        [NeatInterpolation.text|
Error: The specified repository is not a valid URI

You provided the following argument:

↳ $uri

... which is not a valid URI
|]
    UnsupportedURIScheme uri uriScheme ->
        let uriSchemeText = Text.pack uriScheme

        in  [NeatInterpolation.text|
Error: URI schemes other than https are not supported

You specified the following URI:

↳ $uri

... which has the following scheme:

↳ $uriSchemeText

... which is not https
|]

    UnsupportedDomain uri uriRegName ->
        let uriRegNameText = Text.pack uriRegName

        in  [NeatInterpolation.text|
Error: Domains other than github.com are not supported

You specified the following URI:

↳ $uri

... which has the following domain:

↳ $uriRegNameText

... which is not github.com
|]

    UnsupportedPort uri uriPort ->
        let uriPortText = Text.pack uriPort

        in  [NeatInterpolation.text|
Error: Non-default ports are not supported

You specified the following URI:

↳ $uri

... which has the following explicit port specification:

↳ $uriPortText

... which is not permitted by this tool
|]

    UnsupportedQuery uri uriQuery ->
        let uriQueryText = Text.pack uriQuery

        in  [NeatInterpolation.text|
Error: Non-empty query strings are not supported

You specified the following URI:

↳ $uri

... which has the following query string:

↳ $uriQueryText

... which is not permitted by this tool
|]

    UnsupportedFragment uri uriFragment ->
        let uriFragmentText = Text.pack uriFragment

        in  [NeatInterpolation.text|
Error: Non-empty query fragments are not supported

You specified the following URI:

↳ $uri

... which has the following query fragment:

↳ $uriFragmentText

... which is not permitted by this tool
|]

    NotAValidGitHubRepository uri ->
        [NeatInterpolation.text|
Error: Not a valid GitHub repository

You specified the following URI:

↳ $uri

... which is not a valid GitHub repository.  A valid repository must match the
following format:

↳ https://github.com/$${owner}/$${repository}[.git]
|]

    FailedToFetchGitHubArchive args ->
        let argsText = Text.intercalate " " args

        in  [NeatInterpolation.text|
Error: Failed to fetch the GitHub's repository archive

The following command failed to fetch the following archive for the repository:

    nix-prefetch-url $argsText
|]

    FailedToParseNixPrefetchURL args ->
        let argsText = Text.intercalate " " args

        in  [NeatInterpolation.text|
Error: Failed to parse the nix-prefetch-url output

The following command:

    nix-prefetch-url $argsText

... should have produced two lines of output:

* First the SHA256 hash of the GitHub project
* Then the /nix/store/… path of the downloaded project

However, the output did not match, possibly indicating an internal error, either
with this tool or with nix-prefetch-url
|]

    FailedToCloneRepository args ->
        let argsText = Text.intercalate " " args

        in  [NeatInterpolation.text|
Error: Failed to clone the GitHub repository

The following command failed to clone the repository:

    nix-prefetch-git $argsText
|]

    FailedToParseNixPrefetchGit args message ->
        let argsText = Text.intercalate " " args

            messageText = Text.pack message

        in  [NeatInterpolation.text|
Error: Failed to parse the output of nix-prefetch-git

The following command:

    nix-prefetch-git $argsText

... should have produced a JSON output matching the following shape:

↳ { url : Text
  , rev : Text
  , path : Text
  , sha256 : Text
  , fetchSubmodules : Bool
  }

... but JSON decoding failed with the following error:

↳ $messageText
|]

    MissingFile expressionFile ->
        let expressionFileText = Turtle.format fp expressionFile

        in  [NeatInterpolation.text|
Error: Missing file

The following file does not exist:

↳ $expressionFileText

Perhaps you meant to specify a different file within the project using the
--file option?
|]
